//! 程序的构建脚本，包括Rust和C++的互操作部分的实际编译/代码转换命令。
//! 这个文件可以改。
//! 下面BINDGEN_RS_FILE_HEADER的提示是给bindgen生成的rs文件的。

use core::panic;
use std::{
    fs::create_dir_all,
    path::{Path, PathBuf},
    process::Command,
    str::FromStr,
};

use convert_case::{Case, Casing};
use static_init::dynamic;

/// 项目列表
/// 添加新项目或修改已有项目时，修改此静态变量即可。
#[dynamic]
static EXTERNAL_PROJECTS: Vec<ExternalProject> = vec![{
    let env_tis_dir = std::env::var_os("TIS_GRABBER_DLL_DIR").map(|os_str| {
        PathBuf::from_str(os_str.to_string_lossy().as_ref())
            .expect("TIS_GRABBER_DLL_DIR is set but not a vaild path.")
    });
    let tis_dir_list = [
        env_tis_dir.clone(),
        env_tis_dir.map(|path| path.join("TIS Grabber DLL")),
        Some(CRATE_DIR.join("..").join("TIS Grabber DLL")),
    ];
    let tis_dir = tis_dir_list
        .into_iter()
        .flatten()
        .find(|dir| dir.join("include").join("tisgrabber.h").is_file())
        .unwrap_or_else(|| {
            panic!("TISGrabber Lib Not Found, Please set TIS_GRABBER_DLL_DIR environment variable.")
        });
    ExternalProject {
        project_name: "TISGrabber".to_string(),
        project_dir: tis_dir.clone(),
        extra_headers: vec![
            tis_dir.join("include").join("TISGrabberGlobalDefs.h"),
            tis_dir.join("include").join("tisgrabber.h"),
        ],
        extra_lib_dirs: vec![tis_dir.join("bin").join("x64")],
        extra_lib_names: vec!["tisgrabber_x64".to_string()],
        auto_build: false,
    }
}];

/// 表示要导入的项目。
/// 默认引入的头文件：`{project_dir}/src/export.hpp`文件。不需要将其填入`extra_header_paths`中。
/// 默认链接的库：{project_name}。不需要将其填入`extra_lib_names`中。
/// 默认链接的库文件夹：
/// - Windows下：`{project_dir}/build-rs/Release`
/// - Linux下：`{project_dir}/build-rs`
///
/// 不需要将以上目录填入`extra_header_paths`中。
struct ExternalProject {
    project_name: String,
    project_dir: PathBuf,
    extra_headers: Vec<PathBuf>,
    extra_lib_dirs: Vec<PathBuf>,
    extra_lib_names: Vec<String>,
    auto_build: bool,
}

/// 此crate所在目录
#[dynamic]
static CRATE_DIR: PathBuf = {
    let current_dir = std::env::current_dir().expect("Failed to get current dir!");
    [
        current_dir.clone(),
        current_dir.join("tisgrabber-sys"),
        current_dir.join("test-tisgrabber-sys"),
        current_dir.join(".."),
        current_dir.join("..").join(".."),
        current_dir.join("..").join("..").join(".."),
    ]
    .into_iter()
    .chain(std::env::split_paths(
        std::env::var_os("Path")
            .expect("Failed to find system path!")
            .as_os_str(),
    ))
    // 寻找.gitignore
    .find(|dir| dir.join(".gitignore").is_file())
    .expect("Unable to find workspace dir. Please change current dir to the workspace.")
};

/// 从C++项目导入的Rust文件目录
#[dynamic]
static IMPORT_HEADER_RESULT_DIR: PathBuf = {
    let bindgen_dir = CRATE_DIR.join("src").join("cpp_imports");
    if !bindgen_dir.is_dir() {
        create_dir_all(&bindgen_dir).expect("Failed to create bindgen output dir");
    }
    bindgen_dir
};

/// 以下内容是给src/cpp_bindings目录下、由bindgen生成的rs文件的，不是给此文件（build.rs）的。
const BINDGEN_RS_FILE_HEADER: &str = "/*
 * 注意：不要修改此文件。
 * 提示：请注意在父模块中包含此文件，尤其是修改CMake项目的项目名时。
 * 此文件由bindgen生成，转换自特定的一个或多个C++头文件。详见build.rs文件。
 */
#![allow(unused)]
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
#![allow(non_upper_case_globals)]
#![allow(clippy::upper_case_acronyms)]
";

static ENCODING: &encoding_rs::Encoding = encoding_rs::UTF_8;

fn main() {
    // 打印当前工作区目录。
    // 可通过 cargo build -vv 查看构建输出。
    println!(
        "Current work directory is {:?}",
        CRATE_DIR.to_string_lossy().as_ref()
    );
    // 导入robot_cpp中的项目的头文件至此crate
    for ExternalProject {
        project_name,
        project_dir,
        extra_headers,
        ..
    } in EXTERNAL_PROJECTS.iter()
    {
        import_project_header(project_name, project_dir, extra_headers);
    }
    // 链接robot_cpp中的项目的库
    for ExternalProject {
        project_name,
        project_dir,
        extra_lib_dirs,
        extra_lib_names,
        auto_build,
        ..
    } in EXTERNAL_PROJECTS.iter()
    {
        import_project_lib(
            project_name,
            project_dir,
            extra_lib_dirs,
            extra_lib_names.iter().map(String::as_str),
            *auto_build,
        );
    }
}

fn import_project_header(
    project_name: &str,
    project_dir: impl AsRef<Path>,
    extra_header_paths: impl IntoIterator<Item = impl AsRef<Path>>,
) {
    let project_dir = project_dir.as_ref();
    // 默认值
    let src_dir = project_dir.join("src");
    let default_header_paths = [
        src_dir.join("export.hpp"),
        src_dir.join("export.h"),
        src_dir.join(format!("{}.hpp", project_name.to_case(Case::Snake))),
        src_dir.join(format!("{}.h", project_name.to_case(Case::Snake))),
        src_dir.join(format!("{project_name}.hpp")),
        src_dir.join(format!("{project_name}.h")),
        project_dir.join("export.hpp"),
        project_dir.join("export.h"),
        project_dir.join(format!("{}.hpp", project_name.to_case(Case::Snake))),
        project_dir.join(format!("{}.h", project_name.to_case(Case::Snake))),
        project_dir.join(format!("{project_name}.hpp")),
        project_dir.join(format!("{project_name}.h")),
    ];
    let bindgen_output_path =
        IMPORT_HEADER_RESULT_DIR.join(format!("{}.rs", project_name.to_case(Case::Snake)));
    // 将C++头文件导入成Rust文件
    let extra_header_paths: Vec<_> = extra_header_paths.into_iter().collect();
    let header_paths = extra_header_paths
        .iter()
        // .map(|p| p.as_ref())
        .map(AsRef::as_ref)
        .chain(default_header_paths.iter().map(PathBuf::as_path))
        .filter(|path| path.is_file());
    import_cpp_headers_to_rs(header_paths, bindgen_output_path);
}

// 将C++头文件转换后，导入至Rust代码中
fn import_cpp_headers_to_rs(
    header_paths: impl IntoIterator<Item = impl AsRef<Path>>,
    target_rs_file_path: impl AsRef<Path>,
) {
    // bindgen
    let builder = header_paths
        .into_iter()
        .fold(bindgen::Builder::default(), |builder, path| {
            let path_str = path.as_ref().to_string_lossy();
            println!("cargo:rerun-if-changed={}", path_str.as_ref());
            builder.header(path_str.as_ref())
        })
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
    let builder = builder.generate_comments(false); // 使用注释会在cargo test时出现编译错误
    builder
        .generate()
        .expect("Unable to generate cpp2rust bindings!")
        // .write(Box::new(&mut context))
        .write_to_file(&target_rs_file_path)
        .expect("Couldn't write cpp2rust bindings!");
    // Insert Header
    let mut file_string = std::fs::read_to_string(&target_rs_file_path)
        .expect("Failed to read generated rust bindings!");
    file_string.insert_str(0, BINDGEN_RS_FILE_HEADER);
    std::fs::write(&target_rs_file_path, file_string)
        .expect("Failed to rewrite after writing and reading bindings!");
}

fn import_project_lib<'a>(
    project_name: &'a str,
    project_dir: impl AsRef<Path>,
    extra_lib_dirs: impl IntoIterator<Item = impl AsRef<Path>>,
    extra_lib_names: impl IntoIterator<Item = &'a str>,
    auto_build: bool,
) {
    let project_dir = project_dir.as_ref();
    // 默认值：链接库
    let build_rs_dir = project_dir.join("build-rs");
    #[cfg(target_os = "windows")]
    let default_lib_dirs = [
        // Windows
        build_rs_dir.join("Release"),
        project_dir.join("Release"),
        build_rs_dir.to_path_buf(),
        project_dir.to_path_buf(),
    ];
    #[cfg(target_os = "linux")]
    let default_lib_dirs = [
        // Linux
        build_rs_dir,
    ];
    #[cfg(target_os = "windows")]
    let get_lib_file_names = |lib_name: &str| {
        [
            format!("{lib_name}.lib"),
            // format!("{lib_name}.dll"),
        ]
    };
    #[cfg(target_os = "linux")]
    let get_lib_file_names = |lib_name: &str| [format!("lib{lib_name}.a")];
    // 链接库：枚举
    let extra_lib_dirs: Vec<_> = extra_lib_dirs.into_iter().collect();
    let lib_dirs: Vec<&Path> = extra_lib_dirs
        .iter()
        .map(AsRef::as_ref)
        .chain(default_lib_dirs.iter().map(PathBuf::as_path))
        .collect();
    let lib_names: Vec<&str> = extra_lib_names.into_iter().chain([project_name]).collect();
    let possible_lib_paths: Vec<(&str, PathBuf)> = lib_names
        // 从各个目录查找此lib
        .iter()
        .flat_map(|lib_name| {
            lib_dirs
                .iter()
                // 枚举lib路径
                .flat_map(|lib_dir| {
                    get_lib_file_names(lib_name)
                        .into_iter()
                        .map(|lib_file_name| lib_dir.join(lib_file_name))
                })
                .map(move |lib_path| (*lib_name, lib_path))
        })
        .collect();
    // 链接库：筛选
    // 检查枚举的所有lib路径中是否有实际存在的路径
    let get_existing_lib_dirs =
        || -> Vec<&&Path> { lib_dirs.iter().filter(|dir| dir.is_dir()).collect() };
    let get_existing_libs = || -> Vec<&(&str, PathBuf)> {
        possible_lib_paths
            .iter()
            .filter(|(_lib_name, lib_path)| lib_path.is_file())
            .collect()
    };

    let mut existing_libs = get_existing_libs();
    let mut existing_lib_dirs = get_existing_lib_dirs();
    println!("{:?}", existing_libs);
    println!("{:?}", existing_lib_dirs);
    // 如果没有，就进行默认构建
    if existing_libs.is_empty() {
        if auto_build {
            build_project(project_name, project_dir);
            existing_libs = get_existing_libs();
            existing_lib_dirs = get_existing_lib_dirs();
        } else {
            panic!("Auto build disabled and lib not found!");
        }
    }
    // 实际执行链接
    for lib_dir in existing_lib_dirs.into_iter() {
        let lib_dir_str = lib_dir.to_string_lossy();
        println!("cargo:rustc-link-search={}", lib_dir_str.as_ref());
    }
    for (lib_name, lib_path) in existing_libs.into_iter() {
        println!("cargo:rustc-link-lib={}", lib_name);
        let lib_path_str = lib_path.to_string_lossy();
        println!("cargo:rerun-if-changed={}", lib_path_str.as_ref());
    }
}

fn build_project(project_name: &str, project_dir: impl AsRef<Path>) {
    let project_dir = project_dir.as_ref();
    // Closures
    // Part: CMake
    if project_dir.join("CMakeLists.txt").is_file() {
        let build_rs_dir = project_dir.join("build-rs");
        if !build_rs_dir.is_dir() {
            std::fs::create_dir_all(build_rs_dir.as_path())
                .expect("Failed to create build-rs dir!");
        }
        println!("{project_name}: Building with CMake & MSVC");
        // CMake
        #[cfg(target_os = "windows")]
        let cmake_command = r#"cmake -G "Visual Studio 17 2022" .."#;
        #[cfg(target_os = "linux")]
        let cmake_command = r#"cmake -G "Unix Makefiles" .."#;
        execute_command(&build_rs_dir, cmake_command);
        // Devenv
        #[cfg(target_os = "windows")]
        let build_command = format!(r#"devenv.com {project_name}.sln /Build "Release|x64" "#);
        #[cfg(target_os = "windows")]
        execute_command(&build_rs_dir, build_command);
    }
    // Part: Visual Studio Project
    else if project_dir.join(format!("{project_name}.sln")).is_file() {
        println!("{project_name}: Building with Visual Studio");
        // Devenv
        #[cfg(target_os = "windows")]
        let command = format!(r#"devenv.com {project_name}.sln /Build "Release|x64" "#);
        #[cfg(target_os = "windows")]
        execute_command(project_dir, command);
    }
}

/*
 * 工具函数
 */

fn execute_command(cwd: impl AsRef<Path>, command_line: impl AsRef<str>) {
    let command_line = command_line.as_ref();
    println!("Run Command: {command_line}");
    let cmd = shell_words::split(command_line).expect("Failed to split command line!");
    let cmd_result = Command::new(&cmd[0])
        .current_dir(cwd.as_ref())
        .args(&cmd[1..])
        .output()
        .expect("Failed to execute command!");
    if !cmd_result.status.success() {
        panic!(
            "Command failed!\nDir: {}\nCmd:\"{}\"\nStdOut: {}\nStdErr: {}",
            cwd.as_ref().to_string_lossy().as_ref(),
            command_line,
            ENCODING.decode(cmd_result.stdout.as_slice()).0.as_ref(),
            ENCODING.decode(cmd_result.stderr.as_slice()).0.as_ref()
        );
    }
}
